package net.wicp.tams.common.apiext;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;

import javassist.ClassPool;
import javassist.NotFoundException;
import lombok.extern.slf4j.Slf4j;

/**
 * 
 * @author 偏锋书生
 *
 */
@Slf4j
public class ClassLoaderPlugin {
	private URLClassLoader classLoader;

	private final int maxLevel;// 默认为1层，如果有多个插件，需要2层目录

	public URLClassLoader getClassLoader() {
		return classLoader;
	}

	private ClassPool pool = ClassPool.getDefault();

	private Set<String> jars = new HashSet<String>();

	public Set<String> getJars() {
		return jars;
	}

	public ClassPool getPool() {
		return pool;
	}

	public void close() {
		try {
			if (this.classLoader != null) {
				this.classLoader.close();
			}
		} catch (IOException e) {
			log.error("关闭classLoader失败", e);
		} finally {
			this.classLoader = null;
		}
	}

	public ClassLoaderPlugin(String jarfileDir, ClassLoader parent, int maxLevel) {
		this(new File(jarfileDir), parent, maxLevel);
		Validate.validState(new File(jarfileDir).exists(), "classLoader插件所在目录不存在:" + jarfileDir);
	}

	public ClassLoaderPlugin(File jarfileDir) {
		this(jarfileDir, null, 1);
	}

	public ClassLoaderPlugin(File jarfileDir, ClassLoader parent, int maxLevel) {
		this.maxLevel = maxLevel;
		this.classLoader = createClassLoader(jarfileDir, parent);// 它需要maxLevel，所在在后面
	}

	public Class<?> loadClass(String className) throws ClassNotFoundException {

		return classLoader.loadClass(className);
	}

	public void addToClassLoader(final String baseDir, final FileFilter filter) {
		File base = new File(baseDir);
		if (base != null && base.exists() && base.isDirectory()) {
			File[] files = base.listFiles(filter);
			if (files == null || files.length == 0) {
				log.warn("No files added to classloader from lib: " + baseDir + " (resolved as: "
						+ base.getAbsolutePath() + ").");
			} else {
				this.classLoader = replaceClassLoader(classLoader, base, filter);
			}
		} else {
			log.warn("Can't find (or read) directory to add to classloader: " + baseDir + " (resolved as: "
					+ base.getAbsolutePath() + ").");
		}
	}

	public void addToClassLoader(final String baseDir) {
		addToClassLoader(baseDir, new SuffixFileFilter(".jar"));
	}

	private URLClassLoader createClassLoader(final File libDir, ClassLoader parent) {
		if (null == parent) {
			parent = Thread.currentThread().getContextClassLoader();
		}
		return replaceClassLoader(URLClassLoader.newInstance(new URL[0], parent), libDir, new SuffixFileFilter(".jar"));
	}

	private void findElements(java.util.List<URL> inputURLs, final File[] bases, final FileFilter filter,
			int curlever) {
		if (curlever >= maxLevel) {
			return;
		}
		if (ArrayUtils.isEmpty(bases)) {
			return;
		}
		List<File> nextDirs = new ArrayList<>();
		for (File base : bases) {
			if (null != base && base.canRead() && base.isDirectory()) {
				File[] files = base.listFiles(filter);
				if (null != files && files.length > 0) {
					for (int j = 0; j < files.length; j++) {
						try {
							URL element = files[j].toURI().normalize().toURL();
							inputURLs.add(element);
							jars.add(files[j].getPath());
							log.info("Adding '{}' to classloader", element.toString());
							pool.appendClassPath(files[j].getPath());
							log.info("Adding '{}' to pool", element.toString());

						} catch (MalformedURLException e) {
							log.error("load jar file error", e);
							throw new RuntimeException("load jar file error ");
						} catch (NotFoundException e) {
							log.error("add jar to pool error", e);
							throw new RuntimeException("pool addpath error ");
						}
					}
				}

				if (curlever < maxLevel - 1) {
					File[] listFiles = base.listFiles(new FileFilter() {
						@Override
						public boolean accept(File pathname) {
							return pathname.isDirectory();
						}
					});
					nextDirs.addAll(Arrays.asList(listFiles));
				}
			}
		}
		curlever++;
		findElements(inputURLs, nextDirs.toArray(new File[nextDirs.size()]), filter, curlever);
	}

	private URLClassLoader replaceClassLoader(final URLClassLoader oldLoader, final File base,
			final FileFilter filter) {
		if (null != base && base.canRead() && base.isDirectory()) {
			List<URL> inputURLs = new ArrayList<>();
			for (URL url : oldLoader.getURLs()) {
				inputURLs.add(url);
			}
			findElements(inputURLs, new File[] { base }, filter, 0);
			if (inputURLs.size() == 0) {
				log.error("replaceClassLoader base dir:{} is empty", base.getAbsolutePath());
				return oldLoader;
			}
			ClassLoader oldParent = oldLoader.getParent();
			try {
				oldLoader.close();
			} catch (IOException e) {
				log.error("close classload error ", e);
			}
			return URLClassLoader.newInstance(inputURLs.toArray(new URL[inputURLs.size()]), oldParent);
		}
		return oldLoader;
	}

	public int getMaxLevel() {
		return this.maxLevel;
	}

}
